package com.jl.js.core;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Hashtable;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class AbsolutelyFileClassLoader extends ClassLoader {

	private static ClassLoader parent ;
	
	protected Hashtable<?, ?> classCache = new Hashtable<Object, Object>();
	protected Hashtable<String, byte[]> byteCache = new Hashtable<String, byte[]>();
	protected File file = null;
	protected String archive = null;
	protected boolean archiveLoaded = false;
	

	public AbsolutelyFileClassLoader(File file) {
		super(parent);
		this.file = file;
	}

	/**
	 * Try to load classes from an archive first.
	 * 
	 * @param file
	 *            codebase of the archive and classes not found in the archive
	 * @param archive
	 *            path to the archive relative to <code>url</code>
	 */
	public AbsolutelyFileClassLoader(File file, String archive) {
		this(file);
		this.archive = archive;
	}

	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		byte[] b = loadClassData(name);
		return defineClass(name, b, 0, b.length);
	}

	/**
	 * Get the bytes for the class with the given name.
	 * 
	 * @param name
	 *            the class name
	 * 
	 * @return The bytes for the class.
	 * @exception ClassNotFoundException
	 *                the archive or class file could not be read
	 */
	protected synchronized byte[] loadClassData(String name)
			throws ClassNotFoundException {
		// we modify a number of internal data structures...
		String classPath = name.replace('.', '/');
		String classFile = classPath + ".class";

		if (archive != null) {
			// load from archive
			if (!archiveLoaded) {
				try {
					loadArchive(file.toURI().toURL(), archive);
				} catch (MalformedURLException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}

			// XXX: add signature handling!
			if (byteCache.containsKey(classFile)) {
				byte[] bytes = byteCache.get(classFile);
				// and remove the data to free up some memory (the Class is
				// cached!)
				byteCache.remove(classFile);
				return bytes;
			} else {
				throw new ClassNotFoundException("archive: " + archive
						+ " does not contain:" + classFile);
			}
		} else {
			// load individual class file
			BufferedInputStream bis = null;
			ByteArrayOutputStream baos = new ByteArrayOutputStream();

			try {
				// URL classURL = new URL(url.toURI().toURL(), classFile);
				InputStream is = new FileInputStream(file.getAbsoluteFile()
						+ File.separator + classFile);
				bis = new BufferedInputStream(is);

				return inputStreamToByteArray(bis);
			} catch (MalformedURLException mue) {
				throw new ClassNotFoundException(mue.toString());
			} catch (IOException ioe) {
				throw new ClassNotFoundException(ioe.toString());
			} catch (Throwable t) {
				t.printStackTrace();
				throw new ClassNotFoundException(t.toString());
			} finally {
				try {
					if (bis != null) {
						bis.close();
					}
				} catch (IOException e) {
					// ignore

				}
			}
		}

	}

	protected void loadArchive(URL url, String archive)
			throws ClassNotFoundException {
		ZipInputStream zis = null;

		try {
			URL archiveURL = new URL(url, archive);
			InputStream is = archiveURL.openStream();
			BufferedInputStream bis = new BufferedInputStream(is); // buffer the
																	// data
			zis = new ZipInputStream(bis);

			// XXX: handle duplicate entries...
			// synchronization!
			ZipEntry ze;
			while ((ze = zis.getNextEntry()) != null) {
				String name = ze.getName();
				byte[] data = inputStreamToByteArray(zis);
				byteCache.put(name, data);
			}
			archiveLoaded = true;

		} catch (MalformedURLException e) {
			throw new ClassNotFoundException(e.toString());
		} catch (IOException e) {
			throw new ClassNotFoundException(e.toString());
		} finally {
			if (zis != null)
				try {
					zis.close();
				} catch (IOException e) {
					// ignore

				}
		}
	}

	protected byte[] inputStreamToByteArray(InputStream is) throws IOException {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		int nextByte;

		try {
			while ((nextByte = is.read()) != -1)
				baos.write(nextByte);

			return baos.toByteArray();
		} finally {
			baos.close();
		}
	}

	public static void setParentLoader(ClassLoader contextClassLoader) {
		parent = contextClassLoader;
	}

}
